//
// SoapTrace.Net - A soap message monitoring utility. 
//
// Copyright (C) 2005 Matthew Ward
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
// Matthew Ward (mrward@users.sourceforge.net)

using System;
using System.Drawing;
using System.Text;

namespace SoapTrace.Core.Formatters
{
	/// <summary>
	/// Represents text that has ranges of coloured text.
	/// </summary>
	public class FormattedText
	{
		/// <summary>
		/// A collection of strings with a single colour that are contained 
		/// within the formatted text string.
		/// </summary>
		private FormattedTextItemCollection m_Items = new FormattedTextItemCollection( );
		
		/// <summary>
		/// The default text colour associated with the text.
		/// </summary>
		private Color m_DefaultTextColour = Color.Black;
		
		/// <summary>
		/// The formatted text split up into lines.
		/// </summary>
		private LineCollection m_Lines;
		
		/// <summary>
		/// The character indexes for each line.
		/// </summary>
		private int[ ] m_LineIndices;
		
		/// <summary>
		/// Creates a new instance of the <see cref="FormattedText"/>
		/// class.
		/// </summary>
		/// <param name="textColour">The default text colour for any 
		/// text that is not explicitly appended with a colour.</param>
		public FormattedText( Color textColour )
		{
			m_DefaultTextColour = textColour;
		}
		
		/// <summary>
		/// Gets the coloured text items associated with this formatted text.
		/// </summary>
		public FormattedTextItemCollection Items
		{
			get
			{
				return m_Items;
			}
		}
		
		/// <summary>
		/// Appends the contents of a <see cref="FormattedText"/> object 
		/// to this one.
		/// </summary>
		/// <param name="formattedText">A <see cref="FormattedText"/>
		/// object.</param>
		public void Append( FormattedText formattedText )
		{
			foreach( FormattedTextItem Item in formattedText.m_Items )
			{
				Append( Item.Text, Item.Colour );
			}
		}
		
		/// <summary>
		/// Appends a piece of <paramref name="text"/> to the existing
		/// formatted text.  
		/// </summary>
		/// <remarks>
		/// The text will have the colour specified when the 
		/// <see cref="FormattedText"/> class was created.</remarks>
		/// <param name="text">A string to append.</param>
		public void Append( string text )
		{
			Append( text, m_DefaultTextColour );
		}
		
		/// <summary>
		/// Appends a piece of <paramref name="text"/> to the existing
		/// formatted text.  
		/// </summary>
		/// <remarks>
		/// The text will have the colour specified when the 
		/// <see cref="FormattedText"/> class was created.</remarks>
		/// <param name="text">A string to append.</param>
		/// <param name="colour">The colour of the text.</param>
		public void Append( string text, Color colour )
		{
			m_Items.Add( new FormattedTextItem( text, colour ) );
		}
		
		/// <summary>
		/// Gets the index of the character at the specified line number and 
		/// line position.
		/// </summary>
		/// <param name="lineNumber">The line number (starts from 0).</param>
		/// <param name="linePosition">The line position (starts from 0).</param>
		/// <returns>The index of the character at the specified line
		/// number and line position.</returns>
		public int GetCharIndexFromLine( int lineNumber, int linePosition )
		{			
			ValidateCharIndexParameters( lineNumber, linePosition );

			int[ ] LineIndices = GetLineIndices( );
			
			if( lineNumber >= LineIndices.Length )
			{
				throw new ArgumentOutOfRangeException( "lineNumber", "LineNumber does not exist in text." );
			}
			
			int LineCharIndex = LineIndices[ lineNumber ];
			
			if( linePosition > ( LineCharIndex + Lines[ lineNumber ].Length ) )
			{
				throw new ArgumentOutOfRangeException( "linePosition", "LinePosition does not exist in text" );
			}
			
			int CharIndex = LineIndices[ lineNumber ] + linePosition;
			
			return CharIndex;
		}
		
		/// <summary>
		/// Gets the text colour over the specified text range.
		/// </summary>
		/// <param name="startIndex">The character start index.</param>
		/// <param name="length">The length of the text to have its
		/// colour changed.</param>
		/// <returns>The colour of the text over the specified range if it
		/// is one colour; otherwise <see cref="Color.Empty"/>. </returns>
		public Color GetColour( int startIndex, int length )
		{
			Color Colour = Color.Empty;
			
			int ItemIndex;
			int FirstItemCharIndex;
			int LastItemLength;
			FormattedTextItemCollection Items = GetTextItems( out ItemIndex, out FirstItemCharIndex, out LastItemLength, startIndex, length );
			
			if( Items.Count > 0 )
			{
				for( int i = 0; i < Items.Count; ++i )
				{
					FormattedTextItem Item = Items[ i ];
					
					if( i == 0 )
					{
						Colour = Item.Colour;
					}
					else
					{
						if( Colour != Item.Colour )
						{
							Colour = Color.Empty;
							break;
						}
					}
				}
			}
			
			return Colour;
		}
		
		/// <summary>
		/// Gets the text without any formatting information.
		/// </summary>
		public string Text
		{
			get
			{
				return GetText( );
			}
		}
		
		/// <summary>
		/// Changes the text colour over the specified text range.
		/// </summary>
		/// <param name="colour">The new text colour.</param>
		/// <param name="startIndex">The character start index.</param>
		/// <param name="length">The length of the text to have its
		/// colour changed.</param>
		public void SetColour( Color colour, int startIndex, int length )
		{
			ValidateSetColourParameters( startIndex, length );
						
			int ItemIndex;
			int CharIndex;
			int LastItemLength;
			FormattedTextItemCollection OverlappedItems = GetTextItems( out ItemIndex, out CharIndex, out LastItemLength, startIndex, length );
						
			if( OverlappedItems.Count == 1 )
			{
				FormattedTextItem Item = OverlappedItems[ 0 ];
					
				m_Items.RemoveAt( ItemIndex );
									
				if( ( CharIndex + length ) < Item.Text.Length )
				{
					m_Items.Insert( ItemIndex, new FormattedTextItem( Item.Text.Substring( CharIndex + length ), Item.Colour ) );
				}
				
				m_Items.Insert( ItemIndex, new FormattedTextItem( Item.Text.Substring( CharIndex, length ), colour ) );
				
				if( CharIndex > 0 )
				{
					m_Items.Insert( ItemIndex, new FormattedTextItem( Item.Text.Substring( 0, CharIndex ), Item.Colour ) );
				}
			}
			else
			{
				// Remove overlapped items.
				for( int i = 0; i < OverlappedItems.Count; ++i )
				{
					m_Items.RemoveAt( ItemIndex );
				}
				
				FormattedTextItem FirstItem = OverlappedItems[ 0 ];
				FormattedTextItem LastItem = OverlappedItems[ OverlappedItems.Count - 1 ];
				
				string ItemText = GetMergedText( OverlappedItems, CharIndex, length );

				if( LastItemLength < LastItem.Text.Length )
				{
					m_Items.Insert( ItemIndex, new FormattedTextItem( LastItem.Text.Substring( LastItemLength ), LastItem.Colour ) );
				}
				
				m_Items.Insert( ItemIndex, new FormattedTextItem( ItemText, colour ) );
				
				if( CharIndex > 0 )
				{
					m_Items.Insert( ItemIndex, new FormattedTextItem( FirstItem.Text.Substring( 0, CharIndex ), FirstItem.Colour ) );				
				}
			}
		}
		
		/// <summary>
		/// Gets the merged text from the specified items taken into account
		/// the start and end of the text range.
		/// </summary>
		/// <param name="items">The items containing the text.</param>
		/// <param name="start">The index into the first item's string from
		/// where the merging will start.</param>
		/// <param name="length">The number of characters to merge.</param>
		/// <returns>A string containing the text over the specified 
		/// range of characters.</returns>
		private string GetMergedText( FormattedTextItemCollection items, int start, int length )
		{
			int CharCount = 0;
			StringBuilder Text = new StringBuilder( );
			for( int i = 0; i < items.Count; ++i )
			{
				FormattedTextItem Item = items[ i ];
				
				if( i == 0 )
				{
					// First Item.
					Text.Append( Item.Text.Substring( start, Item.Text.Length - start ) );
				
					CharCount += Item.Text.Length - start;
				}
				else
				{
					if( ( Item.Text.Length + CharCount ) > length )
					{
						// Last item.
						Text.Append( Item.Text.Substring( 0, length - CharCount ) );
					}
					else
					{
						Text.Append( Item.Text );
					}

					CharCount += Item.Text.Length;
				}
			}
			
			return Text.ToString( );
		}
		
		/// <summary>
		/// Gets the text items for the given text range.
		/// </summary>
		/// <param name="index">The index of the first text item, in the
		/// given text range, in the <see cref="m_Items"/> 
		/// collection</param>
		/// <param name="firstItemCharIndex">The index of the first character of the
		/// first item that is part of the text range.</param>
		/// <param name="lastItemLength">The number of characters required 
		/// from the last text item to make up the text range.</param>
		/// <param name="start">The character start index.</param>
		/// <param name="length">The length of the text.</param>
		private FormattedTextItemCollection GetTextItems( out int index, out int firstItemCharIndex, out int lastItemLength, int start, int length )
		{
			FormattedTextItemCollection Items = new FormattedTextItemCollection( );
			
			int ItemCharCount = 0;
			int CharIndex = 0;
			
			firstItemCharIndex = -1;
			index = -1;
			lastItemLength = -1;
			
			for( int i = 0; i < m_Items.Count; ++i )
			{
				FormattedTextItem Item = m_Items[ i ];
				
				if( start < ( CharIndex + Item.Text.Length ) )
				{
					Items.Add( Item );
					
					if( index == -1 )
					{
						// First item in range.
						
						index = i;	
						firstItemCharIndex = start - CharIndex;
						ItemCharCount += Item.Text.Length - firstItemCharIndex;
					}
					else
					{
						ItemCharCount += Item.Text.Length;
					}
					
					if( ItemCharCount >= length )
					{
						// Found all items in specified text range.
						lastItemLength = Item.Text.Length - ( ItemCharCount - length );
						
						break;
					}
				}
				
				CharIndex += Item.Text.Length;
			}	
			
			return Items;
		}
		
		/// <summary>
		/// Checks the parameters passed to the <see cref="SetColour"/>
		/// method.
		/// </summary>
		private void ValidateSetColourParameters( int startIndex, int length )
		{
			if( startIndex < 0 )
			{
				throw new ArgumentOutOfRangeException( "startIndex", "StartIndex cannot be less than zero." );
			}
			
			if( length < 0 )
			{
				throw new ArgumentOutOfRangeException( "length", "Length must be greater than zero." );
			}
		}
		
		/// <summary>
		/// Checks the parameters passed to <see cref="GetCharIndexFromLine"/> 
		/// are valid.
		/// </summary>
		/// <param name="lineNumber">The line number passed to 
		/// <see cref="GetCharIndexFromLine"/>.</param>
		/// <param name="linePosition">The line position passed to 
		/// <see cref="GetCharIndexFromLine"/>.</param>
		private void ValidateCharIndexParameters( int lineNumber, int linePosition )
		{
			if( lineNumber < 0 )
			{
				throw new ArgumentOutOfRangeException( "lineNumber", "LineNumber cannot be less than zero." );				
			}
			
			if( linePosition < 0 )
			{
				throw new ArgumentOutOfRangeException( "linePosition", "LinePosition cannot be less than zero." );				
			}		
		}
	
		
		/// <summary>
		/// Gets an array of character indices for each text line.
		/// </summary>
		/// <returns>
		/// An array of character indices for each line of text.
		/// </returns>
		private int[ ] GetLineIndices( )
		{
			if( m_LineIndices == null )
			{
				m_LineIndices = new int[ Lines.Count ];
				
				int CurrentLength = 0;
				for( int i = 0; i < Lines.Count; ++i )
				{
					m_LineIndices[ i ] = CurrentLength;
					Line Line = Lines[ i ];
					CurrentLength += Line.Length + Line.Terminator.Length; 
				}
			}
			
			return m_LineIndices;
		}
		
		/// <summary>
		/// Gets the unformatted text string.
		/// </summary>
		/// <returns>The unformatted text string.</returns>
		private string GetText( )
		{
			StringBuilder Text = new StringBuilder( );
			
			foreach( FormattedTextItem Item in m_Items )
			{
				Text.Append( Item.Text );
			}
			
			return Text.ToString( );
		}
		
		/// <summary>
		/// Gets the formatted text as a set of lines.
		/// </summary>
		private LineCollection Lines
		{
			get
			{
				if( m_Lines == null )
				{
					m_Lines = LineCollection.GetLines( GetText( ) );
				}
				
				return m_Lines;
			}
		}
	}
}
